#include <string>
#include <vector>
#include <map>
#include <stack>
#include <set>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
#include <mswsock.h>
#include <versionhelpers.h>
#define in_addr_t UINT32
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <poll.h>
#endif
#include <pthread.h>
#include <signal.h>
#include <getopt.h>
#include <curl/curl.h>
#ifdef USE_LUA
#include <lua.hpp>
#endif
#include "BBS2chProxyConnection.h"
#include "BBS2chProxyThreadInfo.h"
#include "BBS2chProxyAuth.h"
#include "BBS2chProxyHttpHeaders.h"
#ifdef USE_MITM
#include "BBS2chProxySecureSocket.h"
#endif
#include "utils.h"

#define PORT 9080
#define VERSION "20250614"
#define BACKLOG 32
#define NUM_LOCKS 7

#ifndef NO_THREAD_POOL
#ifndef NUM_THREADS_DEFAULT
#define NUM_THREADS_DEFAULT 8
#endif
#include "BBS2chProxyThreadPool.h"
static std::stack<void *>curl_handles;
static int num_threads = NUM_THREADS_DEFAULT;
#endif

char *proxy_server;
long proxy_port;
long proxy_type;
long timeout = 30;
char *user_agent;
char *appKey;
char *hmacKey;
BBS2chProxyHttpHeaders api_auth_headers;
BBS2chProxyHttpHeaders api_dat_headers;
int allow_chunked;
int verbosity;
int curl_features;
unsigned int curl_version_number;
bool accept_https;
int force_5chnet = 1;
int force_5chnet_https;
int force_ipv4;
char *bbsmenu_url;
char *api_server;
BBS2chProxyHttpHeaders bbscgi_headers;
int gikofix;
CURLSH *curl_share;
char *lua_script;
unsigned int api_mode = 1;
unsigned int mitm_mode = 0;
std::vector<std::string> bbscgi_postorder;
unsigned int bbscgi_utf8 = 1;
int api_override;
int direct_dat = 1;
int fool_janestyle = 0;
int talk_to_5ch = 0;
int subject_to_lastmodify = 0;
std::set<std::string> subject_to_lastmodify_hosts;
int manage_bbscgi_cookies;
int bbscgi_confirmation = 1;
int bbscgi_fix_timestamp = 0;
#ifdef DEFAULT_TLS_VERSION_OVERRIDE
/* macro value is available at https://github.com/curl/curl/blob/curl-8_13_0/include/curl/curl.h#L2366 */
long curl_tls_version = DEFAULT_TLS_VERSION_OVERRIDE;
#else
long curl_tls_version = CURL_SSLVERSION_DEFAULT;
#endif
#ifdef DEFAULT_CA_BUNDLE
#undef STR
#undef TO_STR
#define STR(x) #x
#define TO_STR(x) STR(x)
int tls_verify_cert = 1;
std::string tls_ca_bundle = TO_STR(DEFAULT_CA_BUNDLE);
#else
#ifdef DISABLE_CERT_VERIFICATION
int tls_verify_cert = 0;
#else
int tls_verify_cert = 2;
#endif
std::string tls_ca_bundle;
#endif

static pthread_mutex_t lockarray[NUM_LOCKS];

void log_printf(int level, const char *format ...)
{
	if(level > verbosity) return;
	va_list argp;
	va_start(argp, format);
	vfprintf(stderr, format, argp);
	va_end(argp);
	fflush(stderr);
}

struct listener {
	int port;
	int sock;
	int backlog;
	struct sockaddr_in addr;
	listener(in_addr_t _addr, int _port, int _backlog) : port(_port), backlog(_backlog)
	{
		memset(&addr, 0, sizeof(addr));
		addr.sin_family = AF_INET;
		addr.sin_addr.s_addr = htonl(_addr);
		addr.sin_port = htons(_port);
	};
	bool initializeSocket() {
#ifdef _WIN32
		if ((sock = WSASocket(AF_INET, SOCK_STREAM, IPPROTO_TCP, NULL, 0, 0)) == INVALID_SOCKET) {
			fprintf(stderr,"WSASocket: socket initialize error\n");
			return false;
		}
#else
		if (-1 == (sock = socket(AF_INET, SOCK_STREAM, 0))) {
			perror("socket");
			return false;
		}
#endif
		
		int optval=1;
		setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, (char *)&optval, sizeof(optval));
		
		socklen_t addrlen = sizeof(addr);
		if (-1 == bind(sock, (struct sockaddr *)&addr, addrlen)) {
			perror("bind");
			return false;
		}
		
		if (-1 == listen(sock, backlog)) {
			perror("listen");
			return false;
		}
		
		addrlen = sizeof(addr);
		if (-1 == getsockname(sock, (struct sockaddr *)&addr, &addrlen)) {
			perror("getsockname");
			return false;
		}
		return true;
	};
};

static void usage(void)
{
	fprintf(stderr,"usage: proxy2ch [OPTIONS]\n");
	fprintf(stderr,"available options:\n");
	fprintf(stderr,"    -p <port[,port2,...]> : Listen on port <port> (default: %d)\n",PORT);
	fprintf(stderr,"    -t <timeout> : Set connection timeout to <timeout> seconds (default: %ld)\n",timeout);
	fprintf(stderr,"    -a <user-agent> : Overwrite user-agent for connections\n");
	fprintf(stderr,"    -g : Accept all incoming connections (default: localhost only)\n");
	fprintf(stderr,"    -c : Accept HTTP CONNECT method (act as an HTTPS proxy)\n");
	fprintf(stderr,"    -4 : Force IPv4 DNS resolution\n");
	fprintf(stderr,"    -b <backlog> : Set backlog value to <backlog> for listen() (default: %d)\n",BACKLOG);
	fprintf(stderr,"    -s : Force https connection for 5ch.net/bbspink.com URLs\n");
	fprintf(stderr,"    -k : Disable certificate verifications in HTTPS connections\n");
	fprintf(stderr,"    --proxy <server:port> : Use proxy <server:port> for connections\n");
	fprintf(stderr,"    --api <AppKey:HmacKey> : Use API for reading/posting\n");
	fprintf(stderr,"    --api-usage <read|post|all|talk> : Specify operations where API is used (default: read)\n");
	fprintf(stderr,"    --api-auth-ua <user-agent> : Specify user-agent for API authentication\n");
	fprintf(stderr,"    --api-dat-ua <user-agent> : Specify user-agent for dat retrieving via API\n");
	fprintf(stderr,"    --api-auth-xua <X-2ch-UA> : Specify X-2ch-UA for API authentication\n");
	fprintf(stderr,"    --api-dat-xua <X-2ch-UA> : Specify X-2ch-UA for dat retrieving via API\n");
	fprintf(stderr,"    --api-auth-header <header: value> : Set arbitrary header-value pair for API authentication\n");
	fprintf(stderr,"    --api-dat-header <header: value> : Set arbitrary header-value pair for dat retrieving via API\n");
	fprintf(stderr,"    --api-server <server> : Specify gateway server for API\n");
	fprintf(stderr,"    --api-override : Add support for overriding requests which already use API for dat retrieving\n");
	fprintf(stderr,"    --no-direct-dat : Retrieve dat using API or read.cgi\n");
	fprintf(stderr,"    --bbsmenu <URL> : Replace \"5ch.net\" occurrences in links for URL\n");
	fprintf(stderr,"    --chunked : Preserve \"chunked\" transfer encoding\n");
	fprintf(stderr,"    --manage-bbscgi-cookies : Let proxy2ch manage cookies used when posting to 5ch/bbspink\n");
	fprintf(stderr,"    --bbscgi-header <header: value> : Force replace header when sending request to bbs.cgi\n");
	fprintf(stderr,"    --bbscgi-postorder <field1,field2,...> : Specify a field order in request body being sent to bbs.cgi\n");
	fprintf(stderr,"    --bbscgi-utf8 <none|api|all> : Specify whether a request body being sent to bbs.cgi should be converted to UTF-8\n");
	fprintf(stderr,"    --bbscgi-fix-timestamp : Fix timestamp when posting to 5ch/bbspink\n");
	fprintf(stderr,"    --bbscgi-confirmation <none|auto|skip> : Specify how to handle new confirmation scheme for 5ch.net (default: auto)\n");
#ifdef USE_LUA
	fprintf(stderr,"    --bbscgi-lua <path> : Process request header/body being sent to bbs.cgi with a Lua script at <path>\n");
#endif
	fprintf(stderr,"    --verbose : Print logs in detail\n");
	fprintf(stderr,"    --gikofix : Fix invalid HTTP POST body (for old gikoNavi)\n");
	fprintf(stderr,"    --keystore <path> : Use a file at <path> as a persistent storage for MonaKey/cookies\n");
	fprintf(stderr,"    --subject-to-lastmodify <host1,host2,...> : Generate subject.txt from lastmodify.txt for given hosts (all hosts when no hosts are given)\n");
	fprintf(stderr,"    --tls-max <version> : Set maximum TLS version used for https://... URLs\n");
	fprintf(stderr,"    --tls-backend <backend> : Choose TLS backend supported in libcurl\n");
	fprintf(stderr,"    --ca-bundle <path> : Specify PEM-encoded CA certificate bundle path used for TLS certificate verifications\n");
#ifndef NO_THREAD_POOL
	fprintf(stderr,"    --num-threads <num> : Specify number of threads in a thread pool\n");
#endif
#ifdef USE_MITM
	fprintf(stderr,"    --mitm <minimal|all> : Act as MITM proxy when -c option is given (experimental)\n");
	fprintf(stderr,"    --mitm-ca-cert <certpath> : Specify CA certificate in PEM format for MITM proxy\n");
	fprintf(stderr,"    --mitm-ca-key <keypath> : Specify CA private key in PEM format for MITM proxy\n");
	fprintf(stderr,"    --mitm-certgen : Generate self-signed CA certificate and private key, print them in PEM format, and exit\n");
#endif
}

static void *threadMainLoop(void *param)
{
#ifndef _WIN32
	sigset_t signalsToIgnore;
	sigemptyset(&signalsToIgnore);
	sigaddset(&signalsToIgnore, SIGPIPE);
	if (-1 == pthread_sigmask(SIG_BLOCK, &signalsToIgnore, NULL)) {
		perror("pthread_sigmask");
		return NULL;
	}
#endif
	CURL *curl = curl_easy_init();
#ifndef NO_THREAD_POOL
	BBS2chProxyThreadPool<PBBS2chProxyConnection> *pool = reinterpret_cast<BBS2chProxyThreadPool<PBBS2chProxyConnection> *>(param);
#ifndef NO_CURL_REUSE_HTTPS
	bool canReuseHttps = curl_version_number >= 0x75000; /* 7.80.0 and later */
#else
	bool canReuseHttps = false;
#endif
	pool->lock();
	curl_handles.push(curl);
	pool->unlock();
	while (1) {
		PBBS2chProxyConnection connection;
		if (pool->getAndLock(&connection) != 0) {
			curl = curl_handles.top();
			curl_handles.pop();
			pool->unlock();
			break;
		}
		connection->curl = curl_handles.top();
		curl_handles.pop();
		pool->unlock();
		connection->connect();
		pool->lock();
		/* curl_easy_reset does not necessaryly release unused TLS contexts, and
		   it results in waste of memory. This issue seems to be fixed in curl
		   7.80.0 and later, but in the earlier versions the logic below might
		   be useful to reduce memory consumption.
		   Ref: https://github.com/curl/curl/issues/7683 */
		/*if (curl_handles.size() + 2 < num_threads && pool->countInQueue() == 0) {
			curl_easy_cleanup(connection->curl);
			connection->curl = curl_easy_init();
		}*/
		if (!canReuseHttps && connection->isHttps) {
			curl_easy_cleanup(connection->curl);
			connection->curl = curl_easy_init();
		}
		curl_handles.push(connection->curl);
		pool->unlock();
	}
#else
	BBS2chProxyConnection *connection = reinterpret_cast<BBS2chProxyConnection *>(param);
	connection->curl = curl;
	connection->connect();
	delete connection;
#endif
	curl_easy_cleanup(curl);
	return NULL;
}

static void *listen(void *param)
{
	std::vector<listener> *listeners = (std::vector<listener> *)param;
	std::vector<listener>::iterator it;
	for (it = listeners->begin(); it != listeners->end(); it++) {
		log_printf(0,"Listening on port %d...\n",it->port);
		if(it->addr.sin_addr.s_addr == INADDR_ANY) {
			log_printf(0,"WARNING: proxy accepts all incoming connections!\n");
		}
	}
	fflush(stderr);
	int sock_c;
	BBS2chProxyThreadCache cache;
	
#ifndef NO_THREAD_POOL
	BBS2chProxyThreadPool<PBBS2chProxyConnection> pool(num_threads);
	pool.run(threadMainLoop);
#endif
	
#ifdef _WIN32
	fd_set fds;
	int nfds = 0;
	for (it = listeners->begin(); it != listeners->end(); it++) {
		if (nfds < it->sock) nfds = it->sock;
	}
	nfds = nfds + 1;
#else
	std::vector<struct pollfd> fds;
	for (it = listeners->begin(); it != listeners->end(); it++) {
		struct pollfd fd;
		fd.fd = it->sock;
		fd.events = POLLIN;
		fd.revents = 0;
		fds.push_back(fd);
	}
#endif
	while(1) {
#ifdef _WIN32
		FD_ZERO(&fds);
		for (it = listeners->begin(); it != listeners->end(); it++) {
			FD_SET(it->sock, &fds);
		}
		if (select(nfds, &fds, NULL, NULL, NULL) < 0) {
			perror("select");
			continue;
		}
		for (it = listeners->begin(); it != listeners->end(); it++) {
			if (FD_ISSET(it->sock, &fds)) {
				int socket = it->sock;
				int port = it->port;
#else
		if (poll(&fds.front(), fds.size(), -1) < 0) {
			perror("poll");
			continue;
		}
		for (std::vector<struct pollfd>::iterator it = fds.begin(); it != fds.end(); it++) {
			if (it->revents & POLLIN) {
				int socket = it->fd;
				int port = listeners->at(it - fds.begin()).port;
#endif
				struct sockaddr_in tmp_addr;
				socklen_t addrlen = sizeof(tmp_addr);
				if (-1 == (sock_c = accept(socket, (struct sockaddr *)&tmp_addr, &addrlen))) {
					perror("accept");
					continue;
				}
#ifndef NO_THREAD_POOL
				PBBS2chProxyConnection connection(new BBS2chProxyConnection(sock_c, port, &cache));
				pool.add(connection);
#else
				BBS2chProxyConnection *connection = new BBS2chProxyConnection(sock_c, port, &cache);
				connection->run(threadMainLoop);
#endif
				//fprintf(stderr,"accepted on %d\n", port);
			}
		}
	}
}

static void lock_cb(CURL *handle, curl_lock_data data, curl_lock_access access, void *userptr)
{
	pthread_mutex_lock(&lockarray[data]);
}

static void unlock_cb(CURL *handle, curl_lock_data data, void *userptr)
{
	pthread_mutex_unlock(&lockarray[data]);
}

static void init_locks(void)
{
	int i;
	for(i = 0; i< NUM_LOCKS; i++)
		pthread_mutex_init(&lockarray[i], NULL);
}

int main(int argc, char *argv[])
{
	std::vector<listener> listeners;
	std::vector<int> ports;
	int	ch;
	extern char	*optarg;
	extern int optind, opterr;
	int option_index;
	bool global = false;
	int backlog = BACKLOG;
	const char *certpath = NULL, *keypath = NULL;
	const char *keyStorage = NULL;
	int selectedTlsBackendId = 0;
	struct option options[] = {
		{"proxy", 1, NULL, 0},
		{"api", 1, NULL, 0},
		{"api-auth-ua", 1, NULL, 0},
		{"api-dat-ua", 1, NULL, 0},
		{"api-auth-xua", 1, NULL, 0},
		{"api-dat-xua", 1, NULL, 0},
		{"api-auth-header", 1, NULL, 0},
		{"api-dat-header", 1, NULL, 0},
		{"api-server", 1, NULL, 0},
		{"api-usage", 1, NULL, 0},
		{"api-override", 0, NULL, 0},
		{"direct-dat", 0, NULL, 0},
		{"no-direct-dat", 0, NULL, 0},
		{"bbsmenu", 1, NULL, 0},
		{"chunked", 0, NULL, 0},
		{"verbose", 0, NULL, 0},
		{"debug", 0, NULL, 0},
		{"bbscgi-header", 1, NULL, 0},
		{"bbscgi-postorder", 1, NULL, 0},
		{"bbscgi-utf8", 1, NULL, 0},
#ifdef USE_LUA
		{"bbscgi-lua", 1, NULL, 0},
#endif
		{"gikofix", 0, NULL, 0},
		{"fool-janestyle", 0, NULL, 0},
		{"keystore", 1, NULL, 0},
		{"talk-to-5ch", 0, NULL, 0},
		{"subject-to-lastmodify", 2, NULL, 0},
		{"manage-bbscgi-cookies", 0, NULL, 0},
		{"skip-bbscgi-confirmation", 0, NULL, 0},
		{"bbscgi-confirmation", 1, NULL, 0},
		{"bbscgi-fix-timestamp", 0, NULL, 0},
		{"tls-max", 1, NULL, 0},
		{"tls-backend", 1, NULL, 0},
		{"ca-bundle", 1, NULL, 0},
#ifdef USE_MITM
		{"mitm", 1, NULL, 0},
		{"mitm-ca-cert", 1, NULL, 0},
		{"mitm-ca-key", 1, NULL, 0},
		{"mitm-certgen", 0, NULL, 0},
#endif
#ifndef NO_THREAD_POOL
		{"num-threads", 1, NULL, 0},
#endif
		{0, 0, 0, 0}
	};
	
	curl_version_info_data *data = curl_version_info(CURLVERSION_NOW);
	curl_features = data->features;
	curl_version_number = data->version_num;
#if LIBCURL_VERSION_NUM >= 0x073800 // 7.56.0 or later
	const curl_ssl_backend **tlsBackends = NULL;
	if (curl_features & CURL_VERSION_SSL) curl_global_sslset(CURLSSLBACKEND_NONE, NULL, &tlsBackends);
#endif
	BBS2chProxyConnection::acceptEncodingChecker.registerEncodings(data);

	log_printf(0,"proxy2ch version %s with curl %s (TLS/SSL backend: %s)\n",VERSION,data->version,data->ssl_version);
#ifdef USE_LUA
	log_printf(0,"Scripting enabled with " LUA_RELEASE "\n");
#endif
	
	while ((ch = getopt_long(argc, argv, "p:t:ha:gc4b:sk", options, &option_index)) != -1) {
		switch (ch) {
			case 0:
				if(!strcmp(options[option_index].name, "proxy")) {
					char *ptr = strchr(optarg, '@');
					if(!ptr) {
						ptr = strstr(optarg, "://");
						if(ptr) ptr = strchr(ptr+3,':');
						else ptr = strchr(optarg,':');
					}
					else ptr = strchr(ptr+1,':');
					if(!ptr) {
						fprintf(stderr,"Proxy port is not specified, as --proxy=server:port\n");
						return -1;
					}
					proxy_server = (char *)malloc(ptr-optarg+1);
					proxy_port = atoi(ptr+1);
					memcpy(proxy_server,optarg,ptr-optarg);
					proxy_server[ptr-optarg] = 0;
					if(!strncasecmp(optarg,"socks4://",9)) proxy_type = CURLPROXY_SOCKS4;
					else if(!strncasecmp(optarg,"socks5://",9)) proxy_type = CURLPROXY_SOCKS5;
#if LIBCURL_VERSION_NUM >= 0x071200
					else if(!strncasecmp(optarg,"socks4a://",10)) proxy_type = CURLPROXY_SOCKS4A;
					else if(!strncasecmp(optarg,"socks5h://",10)) proxy_type = CURLPROXY_SOCKS5_HOSTNAME;
#endif
				}
				else if(!strcmp(options[option_index].name, "api")) {
					if((curl_features & CURL_VERSION_SSL) == 0) {
						fprintf(stderr,"Your libcurl doesn't support HTTPS; API mode cannot be enabled.\n");
						return -1;
					}
					char *ptr = strchr(optarg, ':');
					if(!ptr) {
						fprintf(stderr,"API keys should be provided as AppKey:HmacKey\n");
						return -1;
					}
					appKey = (char *)malloc(ptr-optarg+1);
					memcpy(appKey,optarg,ptr-optarg);
					appKey[ptr-optarg] = 0;
					char *start = ptr+1;
					ptr = strchr(start, ':');
					if(!ptr) ptr = strchr(optarg, 0);
					hmacKey = (char *)malloc(ptr-start+1);
					memcpy(hmacKey,start,ptr-start);
					hmacKey[ptr-start] = 0;
					/*if(*ptr) {
						x_2ch_ua = (char *)malloc(strlen(ptr+1)+11);
						sprintf(x_2ch_ua,"X-2ch-UA: %s",ptr+1);
					}*/
					//fprintf(stderr,"%s,%s,%s\n",appKey,hmacKey,x_2ch_ua);
					//return 0;
				}
				else if(!strcmp(options[option_index].name, "api-auth-ua")) {
					api_auth_headers.set("User-Agent", optarg);
				}
				else if(!strcmp(options[option_index].name, "api-dat-ua")) {
					api_dat_headers.set("User-Agent", optarg);
				}
				else if(!strcmp(options[option_index].name, "api-auth-xua")) {
					api_auth_headers.set("X-2ch-UA", optarg);
				}
				else if(!strcmp(options[option_index].name, "api-dat-xua")) {
					api_dat_headers.set("X-2ch-UA", optarg);
				}
				else if(!strcmp(options[option_index].name, "api-auth-header")) {
					PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(optarg, strlen(optarg));
					if (parsedHeader) {
						api_auth_headers.add(parsedHeader->getName(), parsedHeader->getValue());
					}
				}
				else if(!strcmp(options[option_index].name, "api-dat-header")) {
					PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(optarg, strlen(optarg));
					if (parsedHeader) {
						api_dat_headers.add(parsedHeader->getName(), parsedHeader->getValue());
					}
				}
				else if(!strcmp(options[option_index].name, "chunked")) {
					allow_chunked = 1;
				}
				else if(!strcmp(options[option_index].name, "verbose")) {
					verbosity = 1;
				}
				else if(!strcmp(options[option_index].name, "debug")) {
					verbosity = 5;
				}
				else if(!strcmp(options[option_index].name, "bbsmenu")) {
					bbsmenu_url = (char *)malloc(strlen(optarg)+1);
					strcpy(bbsmenu_url, optarg);
				}
				else if(!strcmp(options[option_index].name, "api-server")) {
					if(api_server) free(api_server);
					api_server = (char *)malloc(strlen(optarg)+1);
					strcpy(api_server, optarg);
				}
				else if(!strcmp(options[option_index].name, "bbscgi-header")) {
					PBBS2chProxyHttpHeaderEntry parsedHeader = BBS2chProxyHttpHeaders::parse(optarg, strlen(optarg));
					if (parsedHeader) {
						bbscgi_headers.add(parsedHeader->getName(), parsedHeader->getValue());
					}
				}
				else if(!strcmp(options[option_index].name, "bbscgi-postorder")) {
					const char *ptr = optarg;
					while(*ptr == ' ') ptr++;
					while(*ptr) {
						const char *end = strchr(ptr, ',');
						if(end) {
							const char *next = end + 1;
							if(end > ptr) {
								end--;
								while(*end == ' ' && end > ptr) end--;
								bbscgi_postorder.push_back(std::string(ptr, end-ptr+1));
							}
							ptr = next;
							while(*ptr == ' ') ptr++;
							continue;
						}
						end = strchr(ptr, 0);
						while(*end == ' ' && end > ptr) end--;
						if(end > ptr) bbscgi_postorder.push_back(std::string(ptr, end-ptr));
						break;
					}
				}
				else if(!strcmp(options[option_index].name, "bbscgi-utf8")) {
					if(!strcmp(optarg, "none")) bbscgi_utf8 = 0;
					else if(!strcmp(optarg, "api")) bbscgi_utf8 = 1;
					else if(!strcmp(optarg, "all")) bbscgi_utf8 = 2;
					else {
						fprintf(stderr, "A value for --bbscgi-utf8 must be one of [none, api, all]\n");
						return -1;
					}
				}
#ifdef USE_LUA
				else if(!strcmp(options[option_index].name, "bbscgi-lua")) {
					lua_script = (char *)malloc(strlen(optarg)+1);
					strcpy(lua_script, optarg);
				}
#endif
				else if(!strcmp(options[option_index].name, "gikofix")) {
					gikofix = 1;
				}
				else if(!strcmp(options[option_index].name, "api-usage")) {
					if(!strcmp(optarg, "read")) api_mode = 1;
					else if(!strcmp(optarg, "post")) api_mode = 2;
					else if(!strcmp(optarg, "postinclpink")) api_mode = 4;
					else if(!strcmp(optarg, "all")) api_mode = 3;
					else if(!strcmp(optarg, "allinclpink")) api_mode = 5;
					else if(!strcmp(optarg, "talk")) api_mode = 8;
					else {
						fprintf(stderr, "A value for --api-usage must be one of [read, post, postinclpink, all, allinclpink, talk]\n");
						return -1;
					}
				}
				else if(!strcmp(options[option_index].name, "api-override")) {
					api_override = 1;
				}
#ifdef USE_MITM
				else if(!strcmp(options[option_index].name, "mitm")) {
					if(!strcmp(optarg, "minimal")) mitm_mode = 1;
					else if(!strcmp(optarg, "all")) mitm_mode = 2;
					else {
						fprintf(stderr, "A value for --mitm must be one of [minimal, all]\n");
						return -1;
					}
				}
				else if(!strcmp(options[option_index].name, "mitm-ca-cert")) {
					certpath = optarg;
				}
				else if(!strcmp(options[option_index].name, "mitm-ca-key")) {
					keypath = optarg;
				}
				else if(!strcmp(options[option_index].name, "mitm-certgen")) {
					BBS2chProxySecureSocket::generateAndPrintSelfSignedCertificate();
					return 0;
				}
#endif
#ifndef NO_THREAD_POOL
				else if(!strcmp(options[option_index].name, "num-threads")) {
					int num = atoi(optarg);
					if (num < 1) {
						fprintf(stderr, "Number of threads must be greater than 0\n");
						return -1;
					}
					if (num > 64) {
						fprintf(stderr, "Number of threads must be less than or equal to 64\n");
						return -1;
					}
					num_threads = num;
				}
#endif
				else if(!strcmp(options[option_index].name, "keystore")) {
					struct stat st;
					if (stat(optarg, &st) != -1) {
						if (S_ISDIR(st.st_mode)) {
							fprintf(stderr, "The path \"%s\" should be a file, not a directory\n", optarg);
							return -1;
						}
					}
					keyStorage = optarg;
				}
				else if(!strcmp(options[option_index].name, "direct-dat")) {
					direct_dat = 1;
				}
				else if(!strcmp(options[option_index].name, "no-direct-dat")) {
					direct_dat = 0;
				}
				else if(!strcmp(options[option_index].name, "fool-janestyle")) {
					fool_janestyle = 1;
				}
				else if(!strcmp(options[option_index].name, "talk-to-5ch")) {
					talk_to_5ch = 1;
				}
				else if(!strcmp(options[option_index].name, "subject-to-lastmodify")) {
					if (optarg) {
						for (char *ptr = optarg; *ptr;) {
							char *next = strchr(ptr, ',');
							if (!next) {
								subject_to_lastmodify_hosts.insert(std::string(ptr));
								break;
							} else {
								if (next > ptr) subject_to_lastmodify_hosts.insert(std::string(ptr, next-ptr));
								ptr = next + 1;
							}
						}
						subject_to_lastmodify = 2;
					}
					else subject_to_lastmodify = 1;
				}
				else if (!strcmp(options[option_index].name, "manage-bbscgi-cookies")) {
					if (data->version_num >= 0x074d00) { // 7.77.0 or later
						manage_bbscgi_cookies = 1;
					} else {
						fprintf(stderr, "--manage-bbscgi-cookies option only works on 7.77.0 or later.\n");
						return -1;
					}
				}
				else if (!strcmp(options[option_index].name, "skip-bbscgi-confirmation")) {
					bbscgi_confirmation = 2;
				}
				else if (!strcmp(options[option_index].name, "bbscgi-confirmation")) {
					if (!strcmp(optarg, "none")) bbscgi_confirmation = 0;
					else if (!strcmp(optarg, "auto")) bbscgi_confirmation = 1;
					else if (!strcmp(optarg, "skip")) bbscgi_confirmation = 2;
					else {
						fprintf(stderr, "A value for --bbscgi-confirmation must be one of [none, auto, skip]\n");
						return -1;
					}
				}
				else if (!strcmp(options[option_index].name, "bbscgi-fix-timestamp")) {
					bbscgi_fix_timestamp = 1;
				}
				else if (!strcmp(options[option_index].name, "tls-max")) {
#if LIBCURL_VERSION_NUM >= 0x073600 // 7.54.0 or later
					if (data->version_num >= 0x073600) {
						if (!strcmp(optarg, "1.0")) curl_tls_version = CURL_SSLVERSION_MAX_TLSv1_0;
						else if (!strcmp(optarg, "1.1")) curl_tls_version = CURL_SSLVERSION_MAX_TLSv1_1;
						else if (!strcmp(optarg, "1.2")) curl_tls_version = CURL_SSLVERSION_MAX_TLSv1_2;
						else if (!strcmp(optarg, "1.3")) curl_tls_version = CURL_SSLVERSION_MAX_TLSv1_3;
						else {
							fprintf(stderr, "A value for --tls-max must be one of [1.0, 1.1, 1.2, 1.3]\n");
							return -1;
						}
					} else {
						fprintf(stderr, "--tls-max option is only available with curl 7.54.0 or later.\n");
						return -1;
					}
#else
					fprintf(stderr, "Build proxy2ch with curl 7.54.0 or later to use --tls-max option.\n");
					return -1;
#endif
				}
				else if (!strcmp(options[option_index].name, "tls-backend")) {
#if LIBCURL_VERSION_NUM >= 0x073800 // 7.56.0 or later
					if (data->version_num >= 0x073800) {
						if (tlsBackends) {
							for (int i = 0; tlsBackends[i]; i++) {
								if (!strcasecmp(tlsBackends[i]->name, optarg)) selectedTlsBackendId = tlsBackends[i]->id;
							}
							if (!selectedTlsBackendId) {
								fprintf(stderr, "A value for --tls-backend must be one of\n");
								for (int i = 0; tlsBackends[i]; i++) {
									fprintf(stderr, "  %s\n", tlsBackends[i]->name);
								}
								return -1;
							}
						}
						else {
							fprintf(stderr, "Your libcurl doesn't support HTTPS.\n");
							return -1;
						}
					} else {
						fprintf(stderr, "--tls-backend option is only available with curl 7.56.0 or later.\n");
						return -1;
					}
#else
					fprintf(stderr, "Build proxy2ch with curl 7.56.0 or later to use --tls-backend option.\n");
					return -1;
#endif
				}
				else if (!strcmp(options[option_index].name, "ca-bundle")) {
					if (!strcmp(optarg, "@system")) {
						if (tls_verify_cert & 0x8000) tls_verify_cert |= 0x2;
						else {
							tls_ca_bundle.clear();
							tls_verify_cert = 0x8002;
						}
					} else {
						if (!strcmp(optarg, "@curl")) tls_ca_bundle.clear();
						else tls_ca_bundle = optarg;
						if (tls_verify_cert & 0x8000) tls_verify_cert |= 0x1;
						else tls_verify_cert = 0x8001;
					}
				}
				break;
			case 'p':
				for (char *ptr = optarg;;) {
					int port = strtoul(ptr, &ptr, 10);
					if (port < 65536) ports.push_back(port);
					if (*ptr != ',') break;
					ptr++;
				}
				break;
			case 't':
				timeout = atoi(optarg);
				break;
			case 'a':
				user_agent = (char *)malloc(strlen(optarg)+1);
				strcpy(user_agent, optarg);
				break;
			case 'g':
				global = true;
				break;
			case 'c':
				accept_https = true;
				break;
			case '4':
				force_ipv4 = 1;
				break;
			case 'b':
				backlog = atoi(optarg);
				break;
			case 's':
				if((curl_features & CURL_VERSION_SSL) == 0) {
					fprintf(stderr,"Your libcurl doesn't support HTTPS; it does not work with -s option.\n");
					return -1;
				}
				if(strstr(data->ssl_version, "OpenSSL/0") || strstr(data->ssl_version, "OpenSSL/1.0") ||
				   (strstr(data->ssl_version, "LibreSSL/2") && !strstr(data->ssl_version, "LibreSSL/2.9"))) {
					fprintf(stderr,
						"WARNING: OpenSSL < 1.1.0 and LibreSSL < 2.9.0 aren't thread-safe without setting callbacks for mutex. "
						"It may cause unintended crashes when many requests are incoming at the same time.\n");
				}
				force_5chnet_https = 1;
				break;
			case 'k':
				tls_verify_cert = 0;
				break;
			default:
				usage();
				return 0;
		}
	}
#if LIBCURL_VERSION_NUM >= 0x073800 // 7.56.0 or later
#if defined(_WIN32) && defined(PREFER_SCHANNEL_WITH_FALLBACK_ON_WINDOWS)
	if (tlsBackends && !selectedTlsBackendId) {
		bool hasSchannel = false;
		int fallbackBackendId = 0;
		for (int i = 0; tlsBackends[i]; i++) {
			if (tlsBackends[i]->id == CURLSSLBACKEND_SCHANNEL) hasSchannel = true;
			else if (!fallbackBackendId) fallbackBackendId = tlsBackends[i]->id;
		}
		if (hasSchannel && fallbackBackendId) {
			if (IsWindows7OrGreater()) {
				selectedTlsBackendId = CURLSSLBACKEND_SCHANNEL;
			} else {
				selectedTlsBackendId = fallbackBackendId;
			}
		}
	}
#endif
	if (selectedTlsBackendId) {
		curl_global_sslset((curl_sslbackend)selectedTlsBackendId, NULL, NULL);
	}
#endif
	curl_global_init(CURL_GLOBAL_DEFAULT);
	if (data->version_num >= 0x074400) { /* version 7.68.0 or later */
		init_locks();
		curl_share = curl_share_init();
		curl_share_setopt(curl_share, CURLSHOPT_LOCKFUNC, lock_cb);
		curl_share_setopt(curl_share, CURLSHOPT_UNLOCKFUNC, unlock_cb);
		curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS);
#if LIBCURL_VERSION_NUM >= 0x070a03
		curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION);
#endif
/* Shared connection cache is still buggy at the moment!
   See https://github.com/curl/curl/issues/4915 */
#if 0 && LIBCURL_VERSION_NUM >= 0x073900
		curl_share_setopt(curl_share, CURLSHOPT_SHARE, CURL_LOCK_DATA_CONNECT);
#endif
	}
#if LIBCURL_VERSION_NUM >= 0x073800 // 7.56.0 or later
	if (tlsBackends && tlsBackends[0] && tlsBackends[1]) { // has multiple TLS backends
		if (!selectedTlsBackendId) selectedTlsBackendId = getCurrentTlsBackend(NULL);
		for (int i = 0; tlsBackends[i]; i++) {
			if (tlsBackends[i]->id == selectedTlsBackendId) {
				const char *name = tlsBackends[i]->id == 1 ? "OpenSSL (or its forks)" : tlsBackends[i]->name;
				fprintf(stderr, "Using %s as TLS backend\n", name);
				break;
			}
		}
	}
#endif
#if _WIN32 && !defined(DEFAULT_CA_BUNDLE)
	if (tls_verify_cert && !(tls_verify_cert & 0x8000) && (curl_features & CURL_VERSION_SSL)) {
		if (!IsWindows7OrGreater()) {
			tls_verify_cert = 0;
		} else {
			bool isOpenSSL = false;
			if (selectedTlsBackendId == 0) selectedTlsBackendId = getCurrentTlsBackend(NULL);
			if (selectedTlsBackendId != 0) {
				isOpenSSL = (selectedTlsBackendId == 1 /* CURLSSLBACKEND_OPENSSL */);
			} else {
				isOpenSSL = (!strncasecmp(data->ssl_version, "OpenSSL", 7) || !strncasecmp(data->ssl_version, "LibreSSL", 8) || !strncasecmp(data->ssl_version, "BoringSSL", 9));
			}
			if (isOpenSSL && data->version_num <= 0x074600) { // 7.70.0 or earlier
				tls_verify_cert = 0;
			}
		}
	}
#endif
	if (!tls_verify_cert && (curl_features & CURL_VERSION_SSL)) {
		fprintf(stderr, "WARNING: TLS certificate verification is disabled.\n");
	}
	if (!api_server) {
		if (api_mode & 8) api_server = strdup("api.talk-platform.com");
		else api_server = strdup("api.5ch.net");
	}
	if (direct_dat && (appKey && (api_mode & 1))) {
		fprintf(stderr, "WARNING: API will never be used for .dat retrieving unless --no-direct-dat is given.\n");
	}
#ifdef USE_MITM
	if (mitm_mode) {
		if (!certpath || !keypath) {
			fprintf(stderr, "MITM is enabled but certificate and/or key is not given.\n");
			return -1;
		}
		if (BBS2chProxySecureSocket::initializeCerts(certpath, keypath) != 0) {
			fprintf(stderr, "MITM is enabled but given certificate and/or key is invalid.\n");
			return -1;
		}
		if (!accept_https) {
			fprintf(stderr, "WARNING: --mitm option is given but -c is not given. MITM mode is disabled.\n");
		}
	}
#endif
	log_printf(0, "Global User-Agent: %s\n",user_agent?user_agent:"n/a");
	if(appKey) {
		log_printf(0, "Use API for:");
		if (api_mode & 1) log_printf(0, " reading");
		if (api_mode & 2) log_printf(0, " posting");
		if (api_mode & 4) log_printf(0, " posting (including bbspink)");
		if (api_mode & 8) log_printf(0, " talk boards");
		log_printf(0, "\n");
		if ((api_mode & 1) || (api_mode & 8)) {
			if (user_agent && !strncmp(user_agent, "Monazilla/", strlen("Monazilla/"))) {
				if (!api_auth_headers.has("User-Agent")) api_auth_headers.set("User-Agent", user_agent);
				if (!api_dat_headers.has("User-Agent")) api_dat_headers.set("User-Agent", user_agent);
			}
			log_printf(0, "API gateway server: %s\n",api_server);
			log_printf(0, "User-Agent (for API authentication): %s\n", api_auth_headers.get("User-Agent").c_str());
			log_printf(0, "User-Agent (for API dat retrieving): %s\n", api_dat_headers.get("User-Agent").c_str());
			log_printf(0, "X-2ch-UA (for API authentication): %s\n", api_auth_headers.get("X-2ch-UA").c_str());
			log_printf(0, "X-2ch-UA (for API dat retrieving): %s\n", api_dat_headers.get("X-2ch-UA").c_str());
		}
		if (api_mode & 2)
			fprintf(stderr, "WARNING: posting to 5ch.net using API is prohibited now.\n");
	}
	if(!bbscgi_headers.empty()) {
		log_printf(0, "Custom headers for bbs.cgi:\n");
		for (BBS2chProxyHttpHeaders::iterator it = bbscgi_headers.begin(); it != bbscgi_headers.end(); ++it) {
			log_printf(0, "  %s\n", it->second.c_str());
		}
	}
	if(lua_script) {
		log_printf(0, "Use Lua script %s for bbs.cgi request modification\n", lua_script);
	}
	if(proxy_server) {
		log_printf(0,"Use proxy %s:%ld for connection\n",proxy_server,proxy_port);
	}
	if (manage_bbscgi_cookies) {
		log_printf(0, "NOTE: Cookies for 5ch/bbspink are managed by proxy2ch. Almost all cookies from clients are ignored.\n");
	}
	if (keyStorage) {
		BBS2chProxyConnection::keyManager.setStorage(keyStorage);
		int loaded = BBS2chProxyConnection::keyManager.loadKeys();
		if (loaded > 0) {
			log_printf(0, "Loaded %d cookies from %s\n", loaded, keyStorage);
		} else if (loaded == 0) {
			log_printf(0, "New cookies will be saved to %s\n", keyStorage);
		} else {
			log_printf(0, "Error: The file %s is not empty but does not look like valid JSON.\n", keyStorage);
			BBS2chProxyConnection::keyManager.setStorage("");
		}
	}

	BBS2chProxyConnection::compileRegex();
	
#ifdef _WIN32
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 0), &wsaData) == SOCKET_ERROR) {
		fprintf(stderr, "WSAStartup: error initializing WSA.\n");
		return -1;
	}
	int optval = SO_SYNCHRONOUS_NONALERT;
	setsockopt(INVALID_SOCKET, SOL_SOCKET, SO_OPENTYPE, (char *)&optval, sizeof(optval));
#endif
	
	if (ports.empty()) ports.push_back(PORT);
	for (std::vector<int>::iterator it = ports.begin(); it != ports.end(); it++) {
		listeners.push_back(listener(global ? INADDR_ANY : INADDR_LOOPBACK, *it, backlog));
		if (!listeners.back().initializeSocket()) return -1;
	}
	
#ifndef _WIN32
	struct sigaction sa;
	memset(&sa, 0, sizeof(sa));
	sa.sa_handler = SIG_IGN;
	sigemptyset(&sa.sa_mask);
	if (-1 == sigaction(SIGPIPE, &sa, NULL)) {
		perror("sigaction");
		return -1;
	}
#endif

#if 0
	pthread_t thread_listener;
	if(0 != pthread_create(&thread_listener , NULL , listen , &listeners))
		perror("pthread_create");
	pthread_join(thread_listener, NULL);
#else
	listen(&listeners);
#endif
	return 0;
}
